/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.msoftch.internal;

import java.lang.ref.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

/**
 * {@link ICache} implementation using a {@link ConcurrentHashMap}.
 * 
 * @param <K> The key type
 * @param <V> The value type
 * @author M. Hautle
 */
public class ConcurrentMapCache<K, V> implements ICache<K, V> {
    /** The map used as cache. */
    private Map<Key<K>, Reference<V>> cache = new ConcurrentHashMap<Key<K>, Reference<V>>();

    /** Lock to protect {@link #expungeStaleEntries()}. */
    private final Lock expungeLock = new ReentrantLock();

    /** Reference queue for cleared entries. */
    private final ReferenceQueue<K> queue = new ReferenceQueue<K>();

    /**
     * {@inheritDoc}
     */
    @Override
    public final V get(K key) {
        expungeStaleEntries();
        final Reference<V> ref = cache.get(new LookupKey(key));
        return ref != null ? ref.get() : null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public V put(K key, V value) {
        expungeStaleEntries();
        final Reference<V> ref = cache.put(new Key<K>(key, queue), new SoftReference<V>(value));
        return ref != null ? ref.get() : null;
    }

    /**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
        // there is no need that two threads clean up the references....
        if (!expungeLock.tryLock())
            return;
        try {
            for (Reference<? extends K> ref = queue.poll(); ref != null; ref = queue.poll())
                cache.remove(ref);
        } finally {
            expungeLock.unlock();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clear() {
        cache.clear();
        // just cleanup the queue (also only by one thread :) )
        if (!expungeLock.tryLock())
            return;
        try {
            for (Reference<? extends K> ref = queue.poll(); ref != null; ref = queue.poll())
                ;
        } finally {
            expungeLock.unlock();
        }
    }

    /**
     * Key wrapping the real key into a {@link SoftReference}.
     * 
     * @param <K> The key type
     * @author M. Hautle
     */
    private static class Key<K> extends SoftReference<K> {
        /** The hashcode of the key. */
        private final int hash;

        /**
         * Default constructor.
         * 
         * @param key The key value
         * @param q The reference queue
         */
        public Key(K key, ReferenceQueue<K> q) {
            super(key, q);
            hash = key.hashCode();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int hashCode() {
            return hash;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        @SuppressWarnings("rawtypes")
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            // unwrap key
            if (obj instanceof Key)
                obj = ((Key) obj).get();
            return equals(get(), obj);
        }

        /**
         * Checks two elements on equality.
         * 
         * @param o1 The first object or null
         * @param o2 The second object or null
         * @return True if the parameters were equals
         */
        private boolean equals(Object o1, Object o2) {
            if (o1 == o2)
                return true;
            return o1.equals(o2);
        }
    }

    /**
     * Key wrapping the real key for a lookup.
     * 
     * @author M. Hautle
     */
    private static class LookupKey {
        /** The key itself. */
        private final Object key;

        /**
         * Default constructor.
         * 
         * @param key The key value
         */
        public LookupKey(Object key) {
            this.key = key;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int hashCode() {
            return key.hashCode();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        @SuppressWarnings("rawtypes")
        public final boolean equals(Object obj) {
            // unwrap key
            if (obj instanceof Key)
                obj = ((Key) obj).get();
            if (key == obj)
                return true;
            return key.equals(obj);
        }
    }
}
